/*
 *  Copyright 2012 Anton Van Zyl. http://code.google.com/p/java-swiss-knife/
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */
package com.knife.security;

import java.util.Random;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;

import com.knife.security.exception.InvalidPasswordLength;

/**
 * This is the password utilities that I use the most. <br/>
 * <br/>
 * Please visit <a
 * href="http://code.google.com/p/java-swiss-knife/">Java-Swiss-Knife</a> and
 * comment, rate, contribute or raise a issue/enhancement for my library. <br/>
 * 
 * @author Anton Van Zyl
 * 
 */
public final class PasswordUtil {

	// Bonus scheme for passwords
	private static final int Excess = 3;
	private static final int Upper = 4;
	private static final int Numbers = 3;
	private static final int Symbols = 5;
	private static final String regex = "[!,@,#,$,%,^,&,*,?,_,~]+";

	//@formatter:off
    protected static char[] lowerCaseAlpa = {  
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n',  
        'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 
    };  
    protected static char[] upperCaseAlpha = {  
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N',  
        'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
    };  
    protected static char[] numbers = {  
        '1', '2', '3', '4', '5', '6', '7', '8', '9','0', ' ',
    };  
    protected static char[] symbols = {  
        '+', '-', '@','!','#','$','%','^','&','*','?','_','~' 
    };  
    //@formatter:on

	/**
	 * This will not generate a readable word but a sequence of chars at the
	 * specified length and complexity. The character selection is listed below
	 * that will be used when generating a valid password:
	 * 
	 * <pre>
	 * <code>
	 *   protected static char[] lowerCaseAlpa = {  
	 *         'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n',  
	 *         'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 
	 *     };  
	 *     protected static char[] upperCaseAlpha = {  
	 *         'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N',  
	 *         'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
	 *     };  
	 *     protected static char[] numbers = {  
	 *         '1', '2', '3', '4', '5', '6', '7', '8', '9','0', ' ',
	 *     };  
	 *     protected static char[] symbols = {  
	 *         '+', '-', '@','!','#','$','%','^','&','*','?','_','~' 
	 *     };  
	 * </code>
	 * </pre>
	 * 
	 * @param complexity
	 *            - The complexity that the password will be generated at, this
	 *            will always be equal to or above the specified complexity
	 * @return The password that is generated
	 */
	public static String generateRandomPassword(PasswordComplexity complexity) {

		StringBuilder password = new StringBuilder(generatePassword(complexity));

		try {
			boolean foundPassword = false;
			while (foundPassword == false) {

				PasswordComplexity comp = checkPasswordStrength(password.toString(), complexity.getPasswordLength());
				if (comp.ordinal() >= complexity.ordinal()) {
					foundPassword = true;
				} else {
					new StringBuilder(generatePassword(complexity));
				}

			}
		} catch (InvalidPasswordLength e) {
			e.printStackTrace();
		}

		return password.toString();
	}

	/**
	 * Generates the password
	 * @param complexity
	 * @return password
	 */
	private static String generatePassword(PasswordComplexity complexity) {
		StringBuilder password = new StringBuilder();
		Random random = new Random();
		switch (complexity) {
		case WEAK:
			for (int i = 0; i < complexity.getPasswordLength(); i++) {
				password.append(lowerCaseAlpa[random.nextInt(lowerCaseAlpa.length)]);
			}
			break;
		case BELOW_AVERAGE:
			for (int i = 0; i < (complexity.getPasswordLength() - 1); i++) {
				password.append(lowerCaseAlpa[random.nextInt(lowerCaseAlpa.length)]);
			}
			password.append(numbers[random.nextInt(numbers.length)]);
			break;
		case AVERAGE:
			for (int i = 0; i < (complexity.getPasswordLength() / 2); i++) {
				password.append(lowerCaseAlpa[random.nextInt(lowerCaseAlpa.length)]);
				password.append(upperCaseAlpha[random.nextInt(upperCaseAlpha.length)]);
			}
			while (password.length() < complexity.getPasswordLength()) {
				password.append(lowerCaseAlpa[random.nextInt(lowerCaseAlpa.length)]);
			}
			break;
		case ABOVE_AVERAGE:
			for (int i = 0; i < ((complexity.getPasswordLength()) / 2) - 1; i++) {
				password.append(lowerCaseAlpa[random.nextInt(lowerCaseAlpa.length)]);
				password.append(upperCaseAlpha[random.nextInt(upperCaseAlpha.length)]);
			}
			password.append(numbers[random.nextInt(numbers.length)]);
			while (password.length() < complexity.getPasswordLength()) {
				password.append(lowerCaseAlpa[random.nextInt(lowerCaseAlpa.length)]);
			}
			break;
		case STRONG:
			for (int i = 0; i < (complexity.getPasswordLength() / 3); i++) {
				password.append(lowerCaseAlpa[random.nextInt(lowerCaseAlpa.length)]);
				password.append(upperCaseAlpha[random.nextInt(upperCaseAlpha.length)]);
				password.append(numbers[random.nextInt(numbers.length)]);
			}
			while (password.length() < complexity.getPasswordLength()) {
				password.append(upperCaseAlpha[random.nextInt(upperCaseAlpha.length)]);
			}
			break;
		case SECURE:
			for (int i = 0; i < (complexity.getPasswordLength() / 4); i++) {
				password.append(lowerCaseAlpa[random.nextInt(lowerCaseAlpa.length)]);
				password.append(upperCaseAlpha[random.nextInt(upperCaseAlpha.length)]);
				password.append(numbers[random.nextInt(numbers.length)]);
				password.append(symbols[random.nextInt(symbols.length)]);
			}
			while (password.length() < complexity.getPasswordLength()) {
				password.append(symbols[random.nextInt(symbols.length)]);
			}
			break;
		}
		return password.toString();
	}

	/**
	 * This will calculate the password input strength and return a enumeration
	 * representing the outcome of the calculation.
	 * 
	 * 
	 * @param passwordInput
	 *            - the password to test against
	 * @param minPasswordLength
	 *            - the minimum password length allowed (This is used in the
	 *            calculation and is better to define)
	 * @return <code>PasswordComplexity</code> enumeration defining the password
	 *         complexity state.
	 * @throws InvalidPasswordLength
	 *             - when the entered password is below the specified length
	 */
	public static PasswordComplexity checkPasswordStrength(String passwordInput, int minPasswordLength) throws InvalidPasswordLength {

		if (passwordInput == null) {
			throw new NullPointerException("checkPasswordStrength: passwordInput is NULL");
		}

		int baseScore;
		if (passwordInput.length() >= minPasswordLength) {
			baseScore = 50;
			Password password = analyzePassword(passwordInput, minPasswordLength);
			baseScore = calculateComplexity(password, baseScore);
		} else {
			throw new InvalidPasswordLength("The password is incorrect length [minPasswordLength=" + minPasswordLength + ";passwordInputLength=" + passwordInput.length() + ";]");
		}

		return PasswordComplexity.calculateComplexity(baseScore);
	}

	/**
	 * Analyse a input string and creates a score to be calculated with. This is
	 * build by extracting the different characters that exist in the string.
	 * 
	 * @param passwordInput
	 *            - The string that is the password
	 * @param minPasswordLength
	 *            - The length the password needs to be.
	 * @return - Password object that contains the count
	 */
	private static Password analyzePassword(final String passwordInput, final int minPasswordLength) {
		Password password = new Password();

		for (char value : passwordInput.toCharArray()) {
			String stringValue = String.valueOf(value);
			if (StringUtils.isNumeric(stringValue)) {
				password.numbers++;
			} else if (StringUtils.isAllUpperCase(stringValue)) {
				password.upper++;
			} else if (Pattern.matches(regex, stringValue) || value == ' ') {
				password.symbols++;
			}
		}

		password.excess = passwordInput.length() - minPasswordLength;

		if (password.upper > 0 && password.numbers > 0 && password.symbols > 0) {
			password.bonusCombo = 25;
		} else if ((password.upper > 0 && password.numbers > 0) || (password.upper > 0 && password.symbols > 0) || (password.numbers > 0 && password.symbols > 0)) {
			password.bonusCombo = 15;
		}

		if (StringUtils.isAllLowerCase(passwordInput)) {
			password.bonusFlatLower = -15;
		}

		if (StringUtils.isNumeric(passwordInput)) {
			password.bonusFlatNumber = -35;
		}

		return password;
	}

	/**
	 * Calculates the complexity of the password
	 * 
	 * @param password
	 *            - The password a object that holds the score
	 * @param baseScore
	 *            - the base on which the score proceeds
	 * @return the score that the password received
	 */
	private static int calculateComplexity(final Password password, final int baseScore) {
		int result = baseScore + (password.excess * Excess) + (password.upper * Upper) + (password.numbers * Numbers) + (password.symbols * Symbols) + password.bonusCombo
				+ password.bonusFlatLower + password.bonusFlatNumber;
		return result;

	}

}
